<?php

	namespace org\tekuna\core\application;

	use org\tekuna\base\Tekuna;
	use org\tekuna\base\classloader\DirectoryClassLoader;
	use org\tekuna\base\classloader\PharClassLoader;
	
	use org\tekuna\core\context\Context;
	use org\tekuna\core\configuration\Configuration;
	use org\tekuna\core\configuration\ConfigurationException;
	use org\tekuna\core\event\EventListener;
	use org\tekuna\core\event\EventManager;
	use org\tekuna\core\event\EventException;
	use org\tekuna\core\event\ApplicationRunEvent;
	use org\tekuna\core\plugin\PluginManager;

	
	/**
	 * Abstract application class that offers some helper methods. In a running Tekuna application there will
	 * be either a HttpApplication instance or CliApplication instance running.
	 */
	abstract class Application {

		protected
			$objLogger = NULL,
			
			/** This is the place where the application context is held. */
			$objContext = NULL;
	
		
		/**
		 * Static builder method for a new application. Returns either an instance of HttpApplication
		 * or an instance of CliApplication, dependent on the current request. To determine the
		 * mode, the static method isHttpRequest() is used.
		 * 
		 * @param Configuration $objConfiguration the configuration for the new application
		 * @return Application runnable application instance
		 */
		public static function buildApplication(Configuration $objConfiguration) {
			
			if (self :: isHttpRequest()) {
			
				return new HttpApplication($objConfiguration);
			}
			else {
				
				return new CliApplication($objConfiguration);
			}
		}
		
		
		/**
		 * Returns true if the current request is done via HTTP(S).
		 * 
		 * @return boolean HTTP request indicator
		 */
		public static function isHttpRequest() {
		
			return ! self :: isCliRequest();
		}

		
		/**
		 * Retuns true if the current request is done via the command
		 * line interface (CLI).
		 * 
		 * @return boolean CLI request indicator
		 */
		public static function isCliRequest() {
		
			return defined('STDIN');
		}


		/**
		 * Protected constructor that takes a Configuration object.
		 * Use the static method buildApplication() to get a new instance
		 * of an application.
		 * 
		 * @param Configuration $objConfiguration
		 */
		protected function __construct(Configuration $objConfiguration) {
			
			// get a logger
			$this -> objLogger = Tekuna :: getLogger(__CLASS__);
			
			// build the context
			$this -> objContext = new Context();
			$this -> objContext -> setConfiguration($objConfiguration);
			$this -> objContext -> setEventManager(new EventManager($this -> objContext));
			$this -> objContext -> setPluginManager(new PluginManager());
			
			// autowire all context objects
			$this -> objContext -> autowireContextObjects();
		}
		
		
		/**
		 * This method is used to start the loaded application. The stored 
		 * configuration is loaded fully via the loadConfiguration() method,
		 * the defined listeners are registered and the ApplicationRunEvent
		 * is triggered to run the actual application.
		 * 
		 * @throws ConfigurationException if an error occured while
		 *         registering the listeners or no EventListener handled
		 *         the ApplicationRunEvent.
		 */
		public function run() {
			
			// load the configuration completely
			$objConfig = $this -> objContext -> getConfiguration();
			$objConfig -> loadConfiguration($this -> objContext);

			// load plugins
			$this -> objContext -> getPluginManager() -> loadPlugins($objConfig);
			
			// map namespaces to aspects
			$this -> performAspectMappings();
			
			// register additional class loaders
			$this -> registerClassLoaders();
			
			// register all event listeners from the config
			$this -> registerEventListeners();
	
			// trigger ApplicationRunEvent
			$objEventManager = $this -> objContext -> getEventManager();
			$bEventHandled = $objEventManager -> triggerEvent(new ApplicationRunEvent());
			
			// throw an exception if no EventListener handled the ApplicationRunEvent.
			// it is very likely that a configuration error exists
			if (!$bEventHandled) {
				
				throw new ConfigurationException("There has to be at least one EventListener that handles the org\\tekuna\\core\\event\\ApplicationRunEvent in order to perform something useful.");
			}
		}

		
		/**
		 * This method performs all namespace mappings by handling defined aspect-mapping
		 * elements. This is located here (and not in XmlConfiguration) because of the
		 * fact that plugins always have an XML configuration and so aspect mappings
		 * may always occur throuth plugins.
		 */
		protected function performAspectMappings() {

			// get all aspect mappings
			$objRootElement = $this -> objContext -> getConfiguration() -> getRootElement();
			$arrMapperDefinitions = $objRootElement -> getAllChildElements('core', 'aspect-mapping');

			// iterate all aspect mappings
			foreach ($arrMapperDefinitions as $objMapperElement) {

				// extract namespace and aspect
				$sNamespace = $objMapperElement -> getAttribute('core', 'namespace') -> getValue();
				$sAspect = $objMapperElement -> getAttribute('core', 'aspect') -> getValue();

				$this -> objLogger -> info("Mapping namespace '$sNamespace' to aspect '$sAspect'.");

				// perform the mapping
				$objRootElement -> changeAspect($sNamespace, $sAspect);
			}
		}
		
		
		/**
		 * This method loads additional class loaders from the 
		 * Configuration and registers them.
		 * 
		 * @throws ConfigurationException if both dir and phar
		 *         attributes are missing
		 */
		protected function registerClassLoaders() {
			
			// get all class loaders
			$arrClassLoaderDefinitions = $this -> objContext -> getConfiguration() -> getRootElement() -> getAllChildElements('core', 'classloader');

			// iterate all class loaders
			foreach ($arrClassLoaderDefinitions as $objClassLoaderElement) {

				if ($objClassLoaderElement -> hasAttribute('core', 'dir')) {
					
					// add new DirectoryClassLoader
					$sBaseDir = $objClassLoaderElement -> getAttribute('core', 'dir') -> getValue();
					Tekuna :: getInstance() -> addClassLoader(new DirectoryClassLoader($sBaseDir));
				}
				elseif ($objClassLoaderElement -> hasAttribute('core', 'phar')) {
					
					$sPharPath = $objClassLoaderElement -> getAttribute('core', 'phar') -> getValue();
					
					if ($objClassLoaderElement -> hasAttribute('core', 'phar-basedir')) {
					
						// add new PharClassLoader with other base path
						$sPharBaseDir = $objClassLoaderElement -> getAttribute('core', 'phar-basedir') -> getValue();
						Tekuna :: getInstance() -> addClassLoader(new PharClassLoader($sPharPath, $sPharBaseDir));
					}
					else {
						
						// add new PharClassLoader with default base path
						Tekuna :: getInstance() -> addClassLoader(new PharClassLoader($sPharPath));
					}
				}
				else {
					
					throw new ConfigurationException("The classloader definition must have either a 'dir' or a 'phar' attribute.");
				}
			}
		}
		
		
		/**
		 * This method loads all event listeners from the 
		 * Configuration and registers them in the EventManager
		 * 
		 * @throws ConfigurationException if non-existent class 
		 *         or not implementing EventListener class is 
		 *         configured as event listener
		 */
		protected function registerEventListeners() {
			
			$objEventManager = $this -> objContext -> getEventManager();
			
			// get all listeners
			$arrListenerDefinitions = $this -> objContext -> getConfiguration() -> getRootElement() -> getAllChildElements('core', 'listener');

			// iterate all listeners
			foreach ($arrListenerDefinitions as $objListenerElement) {

				// extract namespace and aspect
				$sClass = $objListenerElement -> getAttribute('core', 'class') -> getValue();

				// check if the class exists. This invokes the autoloading of classes implicitly
				if (class_exists($sClass)) {

					// make a new instance of the class
					$objListener = new $sClass();

					// check if this class implements the ConfigHandler interface
					if ($objListener instanceof EventListener) {

						// inject the dependencies
						$objListener -> setApplicationContext($this -> objContext);
						$objListener -> setConfigurationElement($objListenerElement);
						
						$bListenerHasWeight = $objListenerElement -> hasAttribute('core', 'weight');
						
						if ($bListenerHasWeight) {
							
							$sWeight = $objListenerElement -> getAttribute('core', 'weight') -> getValue();
							$objEventManager -> registerListener($objListener, $sWeight);
						}
						else {
							
							$objEventManager -> registerListener($objListener);
						}
					}
					else {
						
						throw new ConfigurationException("The class '$sClass' does not implement the org\\tekuna\\core\\event\\EventListener interface and cannot be used as event listener.");
					}
				}
				else {
					
					throw new ConfigurationException("The class '$sClass' does not exist and cannot be used as event listener.");
				}
			}
		}
	}
